﻿using System.Linq;
using System.Collections.Generic;
using System.Threading;

using System.Net.Sockets;
using System.Net.DnsToolkit.SocketAsync.Enums;

namespace System.Net.DnsToolkit.SocketAsync {
    class RandomPortSelector : IPortSelector {

        private int MinSrcPort = 1;
        private int MaxSrcPort = 49152;
        private Random _randomSrcPort;

        public RandomPortSelector() {
            _randomSrcPort = new Random();
        }

        private int _adressAlreadyInUse;
        public int AdressAlreadyInUse {
            get {
                return _adressAlreadyInUse;
            }
        }

        private int _portCollision;
        public int PortCollision {
            get {
                return _portCollision;
            }
        }

        internal void IncrementAdressAlreadyInUse() {
            Interlocked.Increment(ref _adressAlreadyInUse);
        }
        internal void IncrementPortCollision() {
            Interlocked.Increment(ref _portCollision);
        }


        private byte[] _portInUse = new byte[65536];
        public IEnumerable<int> LockedPorts {
            get {
                return _portInUse
                    .Select((b, i) => { return new { Port = i, Locked = b }; })
                    .Where(p => p.Locked == (byte)PortInUseEnum.Locked)
                    .Select(p => { return p.Port; });

            }
        }
        public void ReleaseSourcePort(int port) {

            // Socket bind failed, no localendpoint
            if (port == -1) {
                return;
            }

            lock (_portInUse) {
                _portInUse[port] = (byte)PortInUseEnum.Free;
            }
        }
 
        public void BindSourcePort(Socket socket) {
        
            if (socket == null) {
                throw new ArgumentNullException("socket");
            }

            int port = 0;
            int collision = 0;
            int maxtry = 10;
            bool randomPortAssigned = false;

            while (!randomPortAssigned && maxtry >= 0) {
                maxtry--;

                lock (_portInUse) {

                    do {
                        port = _randomSrcPort.Next(MinSrcPort, MaxSrcPort);
                        collision++;
                    } while (_portInUse[port] != (byte)PortInUseEnum.Free);

                    if (collision > 1) {
                        IncrementPortCollision();
                    }
                    _portInUse[port] = (byte)PortInUseEnum.InUse;
                    EndPoint ep = new IPEndPoint(0, port);

                    try {

                        socket.Bind(ep);
                        randomPortAssigned = true;

                    } catch (SocketException se) {

                        if (se.SocketErrorCode != SocketError.AddressAlreadyInUse) {
                            throw;
                        } else {
                            IncrementAdressAlreadyInUse();
                            _portInUse[port] = (byte)PortInUseEnum.Locked;
                        }

                    }
                }
            }

            if (maxtry <= 0) {
                throw new InvalidOperationException("bind");
            }

        }

    }
}
